package graphics.engine;

import javax.media.opengl.GL;
import javax.media.opengl.glu.GLU;

import mathematics.Matrix4x4;

import utils.AlignedBox3D;
import utils.Point2D;
import utils.Point3D;
import utils.Ray3D;
import utils.Vector2D;
import utils.Vector3D;

public class Camera {
	
	public Point3D eye;
	public Point3D target;
	public Vector3D up;
	
	public float zNear;
	public float zFar;	
	
	public int viewportWidth;
	public int viewportHeight;
	private int viewportRadius;

	private float orbitingSpeedInDegreesPerRadius = 300;
	
	public float[] ViewMatrix;
	public float[] ProjectionMatrix;
	public boolean isOrthogonal;
	public boolean ProjectionHasChanged = true;
	public boolean ViewHasChanged = true;
	
	public int angle = 35;
	
	private GLU glu;
	
	public Camera()
	{				
		this.glu = new GLU();
		this.zNear = 200.0f;
		this.zFar = 20000.0f;
		
		this.eye = new Point3D(0.0f, 0.0f, this.zFar / 2.0f - 3000);
		this.target = new Point3D(0.0f, 0.0f, 0.0f);
		this.up = new Vector3D(0.0f, 1.0f, 0.0f);

		this.isOrthogonal = true;
	}
	
	public Vector3D getDirection()
	{
		return new Vector3D(this.eye, this.target);
	}
	
	
	public void Reset()
	{
		this.eye = new Point3D(0.0f, 0.0f, this.zFar / 2.0f - 3000);
		this.target = new Point3D(0.0f, 0.0f, 0.0f);
		this.up = new Vector3D(0.0f, 1.0f, 0.0f);

		this.isOrthogonal = true;
		this.isOrthogonal = true;
		this.ProjectionHasChanged = true;
		this.ViewHasChanged = true;
	}
	
	public float[] ViewMatrix()
	{
		Matrix4x4 viewMatrix = new Matrix4x4();
		viewMatrix.setToLookAt(eye, target, up, false);
		
		this.ViewMatrix = viewMatrix.toFloatArray();
		return this.ViewMatrix;
	}
	
	public int[] ViewportMatrix()
	{
		return new int[] { 0,0, viewportWidth, viewportHeight };
	}
	
	public float[] ProjectionMatrix(GL gl)
	{
		gl.glMatrixMode(GL.GL_PROJECTION);
		gl.glLoadIdentity();
		
		glu.gluPerspective(angle , (float)this.viewportWidth / (float)this.viewportHeight, this.zNear, this.zFar);
		
		float[] mat = new float[16];
		gl.glGetFloatv(GL.GL_PROJECTION_MATRIX, mat, 0);
		gl.glLoadIdentity();
		
		this.ProjectionMatrix = mat;
		
		return this.ProjectionMatrix;
	}
	
	public void ZoomUp()
	{
		Vector3D eyeTarget = Point3D.diff(this.eye, this.target);
		float length = (float) eyeTarget.length();
		if(length < this.zFar - (4 * this.zNear))
		{
			Vector3D translationVector = eyeTarget.normalized();
			translationVector = Vector3D.mult(translationVector, length * 5 / 100);
			this.eye = Point3D.sum(this.eye, translationVector);
			this.ViewHasChanged = true;
		}
	}
	
	public void ZoomDown()
	{
		Vector3D eyeTarget = Point3D.diff(this.eye, this.target);
		float length = (float) eyeTarget.length();
		if(length > (2 * this.zNear))
		{
			Vector3D translationVector = eyeTarget.normalized();
			translationVector = Vector3D.mult(translationVector, - length * 5 / 100);
			this.eye = Point3D.sum(this.eye, translationVector);
			this.ViewHasChanged = true;
		}
	}
	
	public void Translate(Vector3D translationVector)
	{
		Vector3D direction = Point3D.diff(this.target, this.eye).normalized();
		Vector3D right = Vector3D.cross(direction, this.up);
		Vector3D translation = Vector3D.sum(
			Vector3D.mult( right,  translationVector.x()  ),
			Vector3D.mult( this.up,  translationVector.y()  )
		);
		translation = Vector3D.mult(translation, 2.0f /*/ (this.ZoomLevel() */);

		this.eye = Point3D.sum(this.eye, translation);
		this.target = Point3D.sum(this.target, translation);
		
		this.ViewHasChanged = true;
	}
	
	public void Translate(Vector2D translationVector)
	{
		Vector3D newVector = new Vector3D(translationVector.x(), translationVector.y(), 0.0f);
		this.Translate(newVector);
	}
	
	public void orbit( float old_x_pixels, float old_y_pixels, float new_x_pixels, float new_y_pixels)
	{
		float pixelsPerDegree = viewportRadius / orbitingSpeedInDegreesPerRadius;
		float radiansPerPixel = 1 / pixelsPerDegree * (float)Math.PI / 180;

		Vector3D targetToEye = Point3D.diff(this.eye, this.target);
		
		Vector3D right = (Vector3D.cross(this.up, targetToEye)).normalized();
		Matrix4x4 rotationMatrix = new Matrix4x4();
		rotationMatrix.setToRotation( (old_y_pixels-new_y_pixels) * radiansPerPixel, right );
		targetToEye = Matrix4x4.mult(rotationMatrix, targetToEye);
		Vector3D newUp = Matrix4x4.mult(rotationMatrix, this.up);
		
			
		rotationMatrix.setToRotation( - (old_x_pixels-new_x_pixels) * radiansPerPixel, new Vector3D(0.0f, 0.0f, 1.0f));
		targetToEye = Matrix4x4.mult(rotationMatrix, targetToEye);
		newUp = Matrix4x4.mult(rotationMatrix, newUp);
		Point3D newEye = Point3D.sum(this.target, targetToEye);
		
		Vector3D newTargetToEye = Point3D.diff(newEye, this.target).normalized();
		Vector3D newProjectedTargetToEyeNormalized = new Vector3D(newTargetToEye.x(), newTargetToEye.y(), this.target.z()).normalized();
		Vector3D newRight = (Vector3D.cross(newUp, newTargetToEye)).normalized();
		
		double orientedAngle = Vector3D.computeSignedAngle(newTargetToEye, newProjectedTargetToEyeNormalized, newRight);

		if(orientedAngle >= 0 )
		{
			this.eye= newEye;
			this.up = newUp;	
			this.isOrthogonal = false;
			this.ViewHasChanged = true;
		}
		else if (orientedAngle >= -1)
		{

		}
	}

	public void orbit(Vector3D vector)
	{
		this.orbit(0.0f, 0.0f, (float)vector.x(), (float)vector.y());
	}
	
	public void Resize(int width, int height)

	{
		this.viewportWidth = width;
		this.viewportHeight = height;
		viewportRadius = this.viewportWidth < this.viewportHeight
				? (int)(0.5f*this.viewportWidth) : (int)(0.5f*this.viewportHeight);
	}
	
	public void CenterTo(AlignedBox3D box)
	{
		this.target = new Point3D( (box.getMax().x() + box.getMin().x()) / 2.0f, (box.getMax().y() + box.getMin().y()) / 2.0f, target.z());
		this.eye = new Point3D( (box.getMax().x() + box.getMin().x()) / 2.0f, (box.getMax().y() + box.getMin().y()) / 2.0f, eye.z());
	}
	
	public void SetZoom(float xMin, float xMax, float yMin, float yMax)
	{

	}
	
	
	public Ray3D getRay(Point2D source)
	{
		if(this.ViewMatrix != null)
		{
			Matrix4x4 invView = Matrix4x4.Inverse(this.ViewMatrix);
			float[] proj = this.ProjectionMatrix;
			
			
			float x = (float) (((( 2.0f * source.x()) / this.viewportWidth) - 1) / proj[0]);
			float y = (float) (((( 2.0f * source.y()) / this.viewportHeight) - 1) / proj[5]);
			float z = -1.0f;
				
			Vector3D dir = new Vector3D(x, y, z);
			
			dir = Matrix4x4.mult(invView, dir);
		
			
			float[] invViewFloat = invView.toFloatArray();
			
			Point3D origin = new Point3D(invViewFloat[12], invViewFloat[13], invViewFloat[14]);
		
			Ray3D ray = new Ray3D(origin, dir);
			return ray;
		}
		else
		{
			return null;
		}
	}
}
